「查詢」是 API 中常見的附加需求,本質上是對資料的過濾(filtering)與篩選。
無論是篩選文章、商品,還是查詢用戶,根據不同條件來過濾資料並獲得結果,可說是大部分專案的必備功能。
在 view 函式中,實作查詢最簡單的方式,就是使用 Django ORM 的過濾方法。例如,我們可以用filter
方法來根據特定條件篩選 QuerySet。
這種方法簡單直接,適合基本的查詢需求。然而,它也有其局限性——隨著欄位與需求的增加,查詢條件可能變得越來越複雜,導致程式碼冗長且難以維護。
為了解決這一問題,Django Ninja 提供了 FilterSchema,讓我們可以用更「結構化」的方式,定義並管理查詢條件。
本文將介紹 FilterSchema,一步步實作與講解,讓你了解如何在 Django Ninja 中使用 FilterSchema,實現更加靈活、模組化的 API 查詢功能。
本文所有的程式碼改動,可參考這個 PR。
上一篇我們提到「取得文章列表」API,還記得我們在〈卷 11:請求(三)查詢參數 - Query Parameters〉為它加上的「依文章標題查詢」功能嗎?
這是目前程式碼的現況:(請留意查詢參數名稱title
)
...
def get_posts(
request: HttpRequest,
title: None | str = Query(None, min_length=2, max_length=10),
) -> QuerySet[Post]:
"""
取得文章列表
"""
posts = Post.objects.all()
if title:
posts = posts.filter(
title__icontains=title).select_related('author')
return posts
那時我們就是用了 Django ORM 的filter
方法!
posts.filter(title__icontains=title).select_related('author')
最後的select_related('author')
是為了避免「N+1」問題,和查詢邏輯無關,可以先不管。
新的需求是,用同一個關鍵字, 同時查詢文章的標題或作者的名字, 只要任一符合就顯示在結果中。(二者滿足其一即可,也可以都符合)
這需求類似於 iThome 鐵人賽官網的這個查詢功能:
不過我們只能查 2 種,而它可以同時查 3 種:題目、簡介、參賽者暱稱。
但本質上是一樣的。
這時如果用傳統方法實作,查詢會長這樣:
from django.db.models import Q
...
posts = posts.filter(
Q(title__icontains=title) | Q(author__name__icontains=title)
).select_related('author')
此時查詢參數不適合再叫title
,因為它要查詢兩個欄位。沒關係,我們之後會改成query
。
這段程式碼還有兩個重點:
Q
是什麼東西?Q
則是 Django ORM 的 Q 物件,它在複雜查詢邏輯中佔據了重要地位。因此,我們有必要先簡單介紹一下。
為了改善多條件查詢時,程式結構複雜的問題,Django 提供了Q
物件。
Q
物件允許我們靈活地組織查詢條件,使用邏輯運算子(如&
、|
)進行條件合併。在處理複雜條件過濾時非常有用。
比如說,我們想要篩選出標題包含「Ninja」並且作者名稱包含「Alice」的文章,可以這樣寫:
posts = posts.filter(
Q(title__icontains='Ninja') & Q(author__name__icontains='Alice')
)
上述寫法,其實就等價於我們常見的:
posts = posts.filter(
title__icontains='Ninja', author__name__icontains='Alice'
)
所以你通常不會在「AND」需求時使用Q
物件。
「OR」查詢條件才是Q
的經典場景。
現在條件改為——文章標題「或」作者名字有「Alice」就行。可以使用|
:
posts = posts.filter(
Q(title__icontains='Ninja') | Q(author__name__icontains='Alice')
)
Q
物件讓查詢更加靈活且清晰,特別是在面對多個可選條件時。
了解傳統查詢方法容易造成程式冗長的問題,並學習了Q
物件的基礎後,我們要開始介紹今天的主角——FilterSchema。
Django Ninja 提供的 FilterSchema,主要的功能是讓查詢語句更加結構化、模組化,避免 view 函式變得冗長、難讀。
而且,與 Schema 中的驗證方法相同,它也一定程度實現了「關注點分離」原則——藉由將查詢邏輯從 view 函式中抽離出來。
不過,我們先不急著一步到位,容我分階段地改進程式碼。
這樣雖然有點笨拙,但你會對 FilterSchema 與複雜查詢的實作,有更深刻的了解。
我們先用 FilterSchema 實現上述的「新需求:同時查詢作者名字」。
在schemas.py
中建立新的 Schema,不過這次是 FilterSchema:
# post/schemas.py
from ninja import Field, FilterSchema, Schema
...
class PostFilterSchema(FilterSchema):
query: str | None = Field(None, min_length=2, max_length=10)
這個 FilterSchema,其實是給「查詢參數(query parameters)」使用的。所以它的欄位(屬性)名稱,就是你認為客戶端應該使用的查詢參數名稱。
因為同時要查「文章標題」和「作者名字」,所以我命名為query
。
接下來,我們在 view 函式中使用它:
@router.get(...)
@paginate(CustomPagination)
def get_posts(
request: HttpRequest,
filters: PostFilterSchema = Query(), # 使用 FilterSchema
) -> QuerySet[Post]:
"""
取得文章列表
"""
posts = Post.objects.all()
if filters.query:
q = Q(title__icontains=filters.query) | \
Q(author__username__icontains=filters.query)
posts = posts.filter(q)
return posts
PS:這裡的專案範例程式碼有誤,第二個Q
查詢誤植為「content__icontains
」,請讀者留意。我已在下一個分支中修復。
看完這個新的 view 函式,你可能不禁心想:
這是在搞笑吧?完全沒有變簡單啊!
沒錯,因為這只是 FilterSchema 的「半成品」,所以看起來比不用還冗長。
儘管如此,其中還是有一些看點,值得我們了解。
q = Q(title__icontains=filters.query) | \
Q(content__icontains=filters.query)
posts = posts.filter(q)
從這段能看出,Q
物件可以單獨進行各種合併操作,最後再丟給 Django filter
方法作為參數。
本例中,這樣的 view 函式參數「句型」在 Django Ninja 非常普遍:
filters: PostFilterSchema = Query()
而初學者看了會很容易「誤解」。
為什麼?因為你可能會以為,filters
的型別是PostFilterSchema
(這沒問題),然後它的預設值是Query()
,因為 Python 函式就是這樣定義的。
但並不是。
Query()
並不是filters
參數的預設值,否則它的型別不應該是Query
嗎?
事實上,= Query()
這段標記不是給你看的,是給 Django Ninja 看的,它相當於是在告訴 Django Ninja:
這個參數內容應該從 HTTP 請求中的查詢參數(query parameters)中取得,而不是從 body 或 path。
這樣想就很容易明白了。
Django Ninja 會試圖從查詢參數獲取字串,拆解它們(如果有複數個查詢字串),然後一一丟給PostFilterSchema
進行初始化與驗證:
這是用「Alice」作為關鍵字的查詢結果:
查到了 30 篇文章,全都來自於作者名稱包含「Alice」的用戶。
本篇先講到這裡,我們已經接觸了兩個新概念——Q 物件和 FilterSchema。
我們還分析了 Django Ninja 中常見的 view 函式參數「句型」,這對於理解框架的使用方式、習慣非常重要。
這些概念需要時間消化,但我可以向你保證,這樣的鋪陳是值得的。
比起直接深入 FilterSchema 的進階用法,這種循序漸進的學習方式更有助於理解。
下一篇你將會看到,為什麼認識 Q 物件很重要。以及如何透過 FilterSchema,來建立結構化、符合「關注點分離」的多欄位查詢。
我們下篇見。
本文同步發表於我的部落格——Code and Me